﻿module JsTypes

open JsAst

type hint = NumberHint | StringHint | NoHint

type JsValue = 
    | Undefined
    | NullValue
    | Number of float
    | Boolean of bool
    | StringValue of string
    | Object of IObject

and PropertyDescriptor(value:JsValue) =
    let _value = value
    member this.GetValue () = _value

and IObject =
    abstract Call : JsValue -> JsValue list -> JsValue
    abstract GetProperty : string -> PropertyDescriptor option
    abstract GetOwnProperty : string -> PropertyDescriptor option
    abstract Get : string -> JsValue
    abstract CanPut : string -> bool
    abstract Put : string -> JsValue -> unit
    abstract HasProperty : string -> bool
    abstract Delete : string -> bool -> bool
    abstract DefaultValue : hint -> JsValue
    abstract DefineOwnProperty : string -> PropertyDescriptor -> bool -> unit
    abstract Construct : JsValue list -> JsValue
    abstract ImplementsCall : bool


(* just a value for now, should be extended with readonly flag *)
type Binding = Binding of JsValue

type EnvironmentRecord() =
    let mutable _bindings = System.Collections.Generic.Dictionary<string, Binding>()
    member this.HasBinding(name) = _bindings.ContainsKey(name)
    member this.GetBindingValue(name) = 
        match _bindings.TryGetValue(name) with
        | true, Binding(value) -> value
        | false, _ -> Undefined
    member this.SetMutableBinding(name, value) =
        do _bindings.[name] <- Binding(value)

    member this.addBindings (bindings:(string*JsValue) list) = 
        for (str, value) in bindings do
            do this.SetMutableBinding(str, value)

and Reference = 
    | PropertyReference of JsValue * string
    | EnvironmentReference of EnvironmentRecord option * string

and Environment = 
    | Frame of EnvironmentRecord*Environment
    | TopFrame of EnvironmentRecord*JsValue
    member this.getReference name = 
        match this with
        | Frame(record, parentEnv) ->
            if record.HasBinding(name)
            then EnvironmentReference(Some record, name)
            else parentEnv.getReference(name)
        | TopFrame(record, rootPrototype) ->
            if record.HasBinding(name)
            then EnvironmentReference(Some record, name)
            else EnvironmentReference(None, name)

    member this.pushFrame (bindings:(string*JsValue) list) = 
        let record = EnvironmentRecord()
        do record.addBindings(bindings)
        Frame(record, this)

    member this.record =
        match this with
        | Frame(record, parentEnv) -> record
        | TopFrame(record, _) -> record

    member this.rootPrototype =
        match this with
        | Frame(record, parentEnv) -> parentEnv.rootPrototype
        | TopFrame(record, rootPrototype) -> rootPrototype

let createTopFrame predefined objectObjectPrototype =
    let record = EnvironmentRecord()
    do record.addBindings(predefined)
    TopFrame(record, objectObjectPrototype)

type EvalStatements = Statement list -> Environment -> JsValue

type NativeObject() =
    let _props = System.Collections.Generic.Dictionary<string, PropertyDescriptor>()
    interface IObject with
        /// Only used by ToPrimitive, gets a defualt value for objects
        (* should call toString or valueOf members *)
        member this.DefaultValue hint =
            match hint with       
            | NumberHint | NoHint -> Number(42.0)  // TODO
            | StringHint -> StringValue("[Object]") // TODO      
        
        member this.DefineOwnProperty name desc throw = 
            do _props.[name] <- desc

        member this.Call thisArg args = failwith "Not callable"
        member this.GetProperty name =
                (this :> IObject).GetOwnProperty name // TODO: prototype inherit
        member this.GetOwnProperty name = 
            match _props.TryGetValue(name) with
            | true, desc -> Some desc
            | false, _ -> None
        member this.Get name = 
            match ((this :> IObject).GetProperty name) with
            | Some desc -> desc.GetValue()
            | None -> Undefined
        member this.CanPut name = false
        member this.Put name value =
            _props.[name] <- PropertyDescriptor(value)
        member this.HasProperty name = _props.ContainsKey(name)            
        member this.Delete name throw = _props.Remove(name)
        member this.Construct args = failwith "Not a constructor"
        member this.ImplementsCall = false
        //member this.Class = "Object"


type NativeFunction(env:Environment, parameters:string list, body:Statement list, evalStmts:EvalStatements) =
    inherit NativeObject()
    let _closureEnv = env
    let _body = body
    interface IObject with
        override this.Call thisArg arguments = 
            let env1 = _closureEnv.pushFrame (List.zip parameters arguments)
            evalStmts body env1
        override this.Construct  arguments =
            let newObj = Object(NativeObject())
            //let proto = this.GetHashCode("prototype")
            let ctorResult = (this :> IObject).Call newObj arguments
            match ctorResult with
            |Object(x)->ctorResult
            |_ -> newObj
        override this.ImplementsCall = true

let ToPrimitive input hint =
    match input with 
     | Object(obj) -> obj.DefaultValue hint
     | _ -> input
    
let parseNumericString str = 42.0 // TODO!

let rec ToNumber =
    function
    | Undefined -> nan
    | NullValue -> 0.0
    | Boolean x as bool ->  if x then 1.0 else 0.0
    | Number(x) -> x
    | StringValue(x) -> parseNumericString x
    | Object(x) as obj-> ToNumber(ToPrimitive obj NumberHint)

let rec ToString jsValue =
    match jsValue with
    | Undefined -> "undefined"
    | NullValue -> "null"
    | Boolean(x) -> if x then "true" else "false"
    | Number(num) -> num.ToString()
    | StringValue(str) -> str
    | Object(x) as obj -> ToString(ToPrimitive obj StringHint)

let rec ToBoolean jsValue =
    match jsValue with
    | Undefined | NullValue -> false
    | Boolean(x) -> x
    | Number(num) -> num <> nan && num <> 0.0
    | StringValue(str) -> str<>""
    | Object(x) as obj -> true

let ToInteger value =
    let num = ToNumber(value)
    if num=nan then 0 else abs(int(num))